package com.googlecode.nls4j;

import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * BundleStorage is a storage of {@link NlsBundle}s which loads and stores
 * bundles for different locales.
 * 
 * @author Andrey Pavlenko
 */
public class BundleStorage<K extends NlsKey<V>, V> {
    private final Class<K> keyType;
    private final ConcurrentHashMap<Locale, NlsBundle<K, V>> bundles;
    private volatile BundleLoader<K, V> bundleLoader;
    private volatile Map<Locale, NlsBundle<K, V>> readOnlyBundles;

    /**
     * Create new BundleStorage with the specified key type and bundle loader.
     * 
     * @param keyType
     *            type of the NlsKeys identifying resources in this bundle
     *            storage.
     * @param bundleLoader
     *            BundleLoader to be used for resource loading.
     * @throws NullPointerException
     *             if one of the arguments is null.
     */
    public BundleStorage(final Class<K> keyType,
            final BundleLoader<K, V> bundleLoader) throws NullPointerException {
        if (keyType == null) {
            throw new NullPointerException("keyType must not be null");
        }
        if (bundleLoader == null) {
            throw new NullPointerException("BundleLoader must not be null");
        }

        this.keyType = keyType;
        this.bundleLoader = bundleLoader;
        bundles = new ConcurrentHashMap<Locale, NlsBundle<K, V>>();
    }

    /**
     * Returns the type of the NlsKeys identifying resources in this bundle
     * storage.
     */
    public Class<K> getKeyType() {
        return keyType;
    }

    /**
     * Returns the bundle loader which is used by this bundle storage for
     * resource loading.
     */
    public BundleLoader<K, V> getBundleLoader() {
        return bundleLoader;
    }

    /**
     * Set a BundleLoader which should be used for resource loading.
     * 
     * @throws NullPointerException
     *             if bundleLoader is null.
     */
    public void setBundleLoader(final BundleLoader<K, V> bundleLoader)
            throws NullPointerException {
        if (bundleLoader == null) {
            throw new NullPointerException("BundleLoader must not be null");
        }
        this.bundleLoader = bundleLoader;
    }

    /**
     * Returns an NlsBundle for the specified locale. If the requesting bundle
     * is not found in this storage, it will be loaded using the BundleLoader
     * and stored in this bundle storage. If the BundleLoader is unable to load
     * the requesting bundle (the method
     * {@link BundleLoader#loadBundle(Class, Locale)} returned null), a default
     * values bundle will be created (using the
     * {@link BundleLoader#getDefaultBundle(Class, Locale)} method) and stored
     * in this bundle storage. This method never returns null.
     * 
     * @throws NullPointerException
     *             if the locale is null.
     */
    public NlsBundle<K, V> getBundle(final Locale locale)
            throws NullPointerException {
        NlsBundle<K, V> b = bundles.get(locale);

        if (b == null) {
            final Class<K> keyType = getKeyType();
            final BundleLoader<K, V> bundleLoader = getBundleLoader();
            b = bundleLoader.loadBundle(keyType, locale);

            if (b == null) {
                b = bundleLoader.getDefaultBundle(keyType, locale);
            }
            if (b == null) {
                throw new NullPointerException(
                        "BundleLoader.getDefaultBundle() returned null");
            }

            final NlsBundle<K, V> b1 = bundles.putIfAbsent(locale, b);
            return (b1 == null) ? b : b1;
        }

        return b;
    }

    /**
     * Add the specified bundle to this bundle storage. If a bundle with the
     * same locale is already exist in this storage, it will be replaced with
     * the specified bundle and returned to the caller.
     * 
     * @throws NullPointerException
     *             if the bundle is null.
     */
    public NlsBundle<K, V> addBundle(final NlsBundle<K, V> bundle)
            throws NullPointerException {
        final NlsBundle<K, V> old = bundles.put(bundle.getLocale(), bundle);
        return old;
    }

    /**
     * Remove a bundle with the specified locale form the storage and return it
     * to the caller.
     * 
     * @throws NullPointerException
     *             if the locale is null.
     */
    public NlsBundle<K, V> removeBundle(final Locale locale)
            throws NullPointerException {
        final NlsBundle<K, V> bundle = bundles.remove(locale);
        return bundle;
    }

    /**
     * Returns an unmodifiable map of bundles this storage contains.
     */
    public Map<Locale, NlsBundle<K, V>> getBundles() {
        if (readOnlyBundles == null) {
            readOnlyBundles = Collections.unmodifiableMap(bundles);
        }
        return readOnlyBundles;
    }

    /**
     * Remove all bundles from this storage.
     */
    public void clear() {
        bundles.clear();
    }
}
